local fs = require("fs")
local getType = require("luv-http.mime").getType
local Cache = require("luv-http.cache")

local readStaticFile = function (filePath, cb)
    fs.stat(filePath, function (err, stat)
        if err or not stat then
            cb(404)
            return
        end
        if stat.type == "directory" then
            cb(404)
            return
        elseif stat.type == "file" then
            cb(200, fs.createReadStream(filePath), stat)
        end
    end)
end

local initCacheIfNecessary = function (ctx)
    local useCache = (
        nil ~= ctx.envCfg.cache
        and nil ~= ctx.envCfg.cache.maxLogCount
        and nil ~= ctx.envCfg.cache.maxAge
        and nil ~= ctx.envCfg.cache.maxRankCount
    )
    local cache = nil
    if useCache then
        if not ctx.envCfg.data then
            ctx.envCfg.data = {}
        end
        cache = ctx.envCfg.data.cacheInst
        if not cache then
            local cacheCfg = ctx.envCfg.cache
            cache = Cache.new(cacheCfg.maxLogCount, cacheCfg.maxAge, cacheCfg.maxRankCount)
            ctx.envCfg.data.cacheInst = cache
        end
    end
    return useCache, cache
end

local getHandler = function (staticDir)
    local handler = {}
    handler.match = function (req, resp, ctx)
        return true
    end
    handler.handle = function (req, resp, ctx)
        local filePath = staticDir .. req.requesturi
        local thread = coroutine.running()
        readStaticFile(filePath, function (respCode, reader, stat)
            if 404 == respCode then
                resp.statusCode = respCode
                coroutine.resume(thread)
            end
            if 200 == respCode then
                local key = req.requesturi
                local useCache, cache = initCacheIfNecessary(ctx)
                local time = nil
                if useCache then
                    time = Cache.getCurrTime()
                    cache.addAccessRecord(key, time) --添加访问记录
                end
                local reqLastModified = req.headers["If-Modified-Since"]
                local respLastModified = os.date("%a, %d %b %Y %X GMT", stat.mtime.sec)
                if reqLastModified == respLastModified then
                    resp.statusCode = 304 -- Not Modified
                    coroutine.resume(thread)
                else
                    resp.statusCode = respCode
                    local contentType = getType(req.requesturi)
                    resp:setHeader("Content-Type", contentType)
                    resp:setHeader("Last-Modified", respLastModified)
                    local cacheData = nil
                    if useCache then
                        cacheData = cache.getCache(key)
                        if cacheData and respLastModified ~= cacheData.tag then --缓存过期
                            cache.delCache(key) --删除过期的缓存
                            cacheData = nil
                        end
                    end
                    if useCache and cacheData then --优先从缓存中获取
                        local data = cacheData.data
                        resp:setHeader("Content-Length", #data)
                        resp:write(data)
                        cache.calcCacheRank(key, time, cacheData)
                        coroutine.resume(thread)
                    else
                        resp:setHeader("Content-Length", stat.size)
                        local chunkAll = ""
                        local realWrite = resp.write
                        function resp:write(chunk, ...)
                            if useCache and (nil == ctx.envCfg.cache.maxSize or stat.size < ctx.envCfg.cache.maxSize) then
                                chunkAll = chunkAll .. chunk
                            end
                            realWrite(self, chunk, ...)
                        end
                        local realEnd = resp._end
                        function resp:_end(...)
                            realEnd(self, ...)
                            resp.write = realWrite
                            resp._end = realEnd
                            if useCache and (nil == ctx.envCfg.cache.maxSize or stat.size < ctx.envCfg.cache.maxSize) then
                                cacheData = {
                                    tag = respLastModified,
                                    data = chunkAll
                                }
                                cache.calcCacheRank(key, time, cacheData)
                            end
                            reader:close()
                            coroutine.resume(thread)
                        end
                        reader:pipe(resp)
                    end
                end
            end
        end)
        coroutine.yield()
    end
    return handler
end

local moduleName = ...
_G[moduleName] = getHandler
complex = getHandler
return complex